delphifandomcom-20200223-history
Creating DLLs
Introduction Delphi allows not only to create applications but allso DLLs. A DLL (short for dynamic link library) is a special kind of executable that cannot be started by itself but exports procedures and functions (called "Entry Points") for other executables to call. Creating a DLL with Delphi Creating a DLL with Delphi consists of two steps: 1. Creating a library project 2. Exporting some functions from that library Creating a library project Delphi has got a special kind of project for a DLL. To create one you use the File -> New -> Other menu entry to open a dialog where you can select the project type you want to create. There is one called "DLL Wizard", that's the one we need here. Select it and press OK and Delphi will automatically create a new DLL project for you. The source code looks like this: library Project1; uses SysUtils, Classes; {$R *.res} begin end. There is also a comment about using strings and ShareMem, but we can ignore that for now. At first glance, this looks like the project source for a normal console program. There is only one difference: It starts with the keyword library rather than program. Another difference is how it is listed in the project explorer. Where an application has got the extension ".exe" a library has got ".dll". So the new project is called "Project1.dll" rather than the usual "Project1.exe". Go ahead and save the new project giving it a name like "MyFirstLibrary". Exporting functions from a library Now lets export some function from our new library. Lets start with something very simple that does not involve complex data structures or a GUI: A function for adding two integers and returning their sum: function AddIntegers(_a, _b: integer): integer; begin Result := _a + _b; end; Yes, that's not very exciting, is it? But it is a function that can be called from any windows program that wants to do it. Side node about calling conventions Now, this is getting rather technical and confusing: Programming languages have evolved over the years to pass parameters and return values from and to function calls in different ways. Originally that was only inside one language and using only one compiler so the programmer just wrote the code and the compiler took care of how the functions were called. Then came DLLs and all of a sudden code written in one language using one compiler could be called from other code written in a different language or only using a different compiler. It turned out that this didn't work, so the compiler makers came up with the notion of calling conventions. We will use one called StdCall here since it allows for most other languages and compilers to call our DLL. (One particular example is Visual Basic (from before dotNET), which supports only the StdCall calling convention.) Exporting the function So let's add a calling convention to our function: function AddIntegers(_a, _b: integer): integer; stdcall; begin Result := _a + _b; end; and export it: exports AddIntegers; Of course in a more complex DLL we would not add all the code to the project source but to units, but we want to keep things simple here. So this is what our source code looks for now: library MyFirstLibrary; uses SysUtils, Classes; {$R *.res} function AddIntegers(_a, _b: integer): integer; stdcall; begin Result := _a + _b; end; exports AddIntegers; begin end. Building the DLL To create a DLL from this library sourcecode is as simple as creating an executable for a normal delphi application: Just build it. You end up with a file called MyFirstLibrary.dll. Looking inside the DLL Now, a DLL is a binary file and if you e.g. load it into an editor, you will see lots of gibberish. But there are tools that can extract some more information about it like DependencyWalker (from Microsoft?) or the PE-Information Wizard in GExperts. If you open your DLL with this wizard, it will tell you the following: (And lots of other things that we don't currently care about.) So it worked, you now have got a DLL that exports the function AddIntegers. Using the DLL So, now how to call that DLL? Simple you just tell the compiler what DLL to load and which function to call with wich parameters. Creating a test project Close the library project (File -> Close All) and create a new console application (File -> New -> Other + Select "Console Application"). You will end up with an empty console application project like this: program Project2; {$APPTYPE CONSOLE} uses SysUtils; begin { TODO -oUser -cConsole Main : Insert code here } end. Save it as "MyLibraryTest" Calling the DLL function Add the following function declaration: function AddIntegers(_a, _b: integer): integer; stdcall; external 'MyFirstLibrary.dll'; And call that function from within the main program: WriteLn(AddIntegers(1, 2)); Write('Press Enter'); ReadLn; Now your project should look like this: program MyLibraryTest; {$APPTYPE CONSOLE} uses SysUtils; function AddIntegers(_a, _b: integer): integer; stdcall; external 'MyFirstLibrary.dll'; begin WriteLn(AddIntegers(1, 2)); Write('Press Enter'); ReadLn; end. Build your project and run it. You should now see a console window like this: Isn't that exciting? You just wrote your first DLL and called it from a test program! ;-) Notes about memory management Now, do you remember that longish comment that Delphi automatically added to the library project? It said something about memory management and strings and ShareMem and stuff. Usually you don't care much about memory management in your applications. That's what the compiler and runtime libraries are for, isn't it? But in the case of DLLs you are interfacing potentially with other compilers and runtime libraries, so you have to think about it. For example, Delphi automatically allocates and frees the memory for storing your strings, it knows when they are no longer needed etc. The same applies for e.g. Visual Basic, but both do it in different ways. So, if you were to pass a string that was allocated by Visual Basic to a DLL written in Delphi you would end up in big trouble, because both would now try to manage the string and would get into each other's hair. Passing strings So, in order to allow strings to be passed between an application and a DLL you must take extra care. Actually, the easiest way is just to not allow it at all! Now I can hear you thinking: "WTF is this guy talking about? Of course I want to pass strings from my application to the dll!" Yes, you would want to, wouldn't you? But you usually don't want to pass strings but you want to pass the series of characters that are stored in a string. To do that you use something that comes from the ancient times when people where using primitive programming languages like C that didn't know such sophisticated data structures as strings (Yes, I know, you C fanatics. C can do everything given the right libraries. This is meant to be a joke.). Back then programmers were stuck with something called zero terminated strings or rather "a pointer to a array of characters which ends with the special character NUL". In Delphi that's called a PChar. Exporting a string function Now lets load our library project again and add a new function to it: function CountChars(_s: Pchar): integer; StdCall; begin Result := Length(_s); end; Notice something? No? Look closer? Still nothing? OK, I'll tell you: You know what the Length function does, don't you? It returns the number of characters stored in a string. But what do we pass to it? What is the parameter _s declared type? It's PChar, not String! Welcome to the conveniences of Delphi: It automatically converts a PChar to a string when assigning it to a string or passing it to a function expecting a string parameter. I hope you also noticed that we declared our new function as StdCall again. You should never forget to explicitly add a calling convention. If you run into access violations when calling a DLL function, that's the first thing to check: Do your program and your DLL use the same calling conventions. If not, you found the problem. We export the new function just the same way we exported the other one. You end up with a project source like this: library MyFirstLibrary; uses SysUtils, Classes; {$R *.res} function AddIntegers(_a, _b: integer): integer; stdcall; begin Result := _a + _b; end; function CountChars(_s: Pchar): integer; StdCall; begin Result := Length(_s); end; exports AddIntegers, CountChars; begin end. Save your changes and compile the project. Calling a string function Now we want to call that new function. So close the library project and open your test project again. Add the following function declaration: function CountChars(_s: Pchar): integer; StdCall; external 'MyFirstLibrary.dll'; and the following code to the main program: WriteLn(CountChars('hello')); Now your program should look like this: program MyLibraryTest; {$APPTYPE CONSOLE} uses SysUtils; function AddIntegers(_a, _b: integer): integer; stdcall; external 'MyFirstLibrary.dll'; function CountChars(_s: Pchar): integer; StdCall; external 'MyFirstLibrary.dll'; begin WriteLn(AddIntegers(1, 2)); WriteLn(CountChars('hello')); Write('Press Enter'); ReadLn; end. Again, notice something? Not? Look closer! What type of parameter to we pass to CountChars? Yes, that's a string constant! Welcome to the conveniences of Delphi again: Delphi automatically converts string constants to PChars if you pass them to a function that expect PChars. Compile and run your program. You should now get another output line saying "5", which incidentally is the number of characters in the string 'hello'. Now, was that painfull? You didn't technically pass a string, but for all practical purposes you did. Now lets try to pass an actual string, not a string constant. Declare a vaiable s and assign your name to it: var s: string; begin s := 'your name'; WriteLn('"', s, '" contains ', CountChars(s)); Write('Press Enter'); ReadLn; end. Compile and run it ... Oh? WTF? It doesn't compile. Welcome to the inconveniences of using PChars! Delphi does not automatically convert strings to PChars (don't ask me, why). You have to explicitly tell it to do that by typecasting. So change your code to: WriteLn('"', s, '" contains ', CountChars(PChar(s))); Compile and run it. You should now get an output like this: "your name" contains 9 characters Press Enter Other Pitfalls In Delphi you are probably used to returning all sorts of data types as function results, e.g. strings or classes. When using DLLs, you should not do that. Only ever return simple data types like integer, double or single, otherwise you will most likely end up with a DLL that you can only use from a Delphi application (and even that will be a major pain in the lower back to get to work). Again, this is a matter of memory management and different compilers / runtime libraries. Array and Record allignments are another annoyance when passing parameters over DLL borders. For performance reasons modern compilers arrang all data types to word- double-word- or even quad-word addresses. But if you don't know what the compilers on both sides actually use, you may get very peculiar effects. Delphi allows you to declare arrays and records as packed, meaning that no allignment takes place, but other programming languages or compilers may not have that concept. Delphi also allows you to force some alignment using the $A or $Align compiler directive, again, other other programming languages or compilers may not give you that option. It's sometimes up to trial and error to find out how a structure is being passed to your program. Your best bet is usually to use data types on both sides that are multiples of 4 bytes long. Conclusion Writing DLLs in Delphi is simple and also powerfull. If you are going to call these DLLs only from other Delphi applications, you might want to look into packages first. If you want to call Delphi code from other programming languages, you may want to look into COM servers (but be warned: While using COM servers from e.g. Visual Basic is very simple, it can be complex from other languages and can be a pain to have to register them every time before being able to call them).